AWS CloudFormation Guard の単体テスト機能を試してみた
AWS CloudFormation Guard では単体テスト(Unit Testing)の機能が提供されているため試してみました。今回のブログでは、AWS Config カスタムルールのテストを行っています。
単体テストの関連ドキュメント
単体テスト機能は次のドキュメントで仕様が公開されています。
cloudformation-guard/UNIT_TESTING.md at main · aws-cloudformation/cloudformation-guard
AWS ユーザーガイドでは次のページです。
Testing AWS CloudFormation Guard rules - AWS CloudFormation Guard
test - AWS CloudFormation Guard
単体テストを試してみた
単体テストのために 2 つのファイルを用意します。
- AWS CloudFormation Guard ルールファイル
- 単体テストファイル(JSON or YAML)
#1
の Guard ルールファイルは AWS Config カスタムルールに設定しているコードそのものです。今回のテストでは、デフォルト VPC を利用しているかを確認するルールでテストを試してみます。
rule no_default_vpc { configuration.isDefault == false }
#2
の単体テストファイル次の形式となります。JSON もしくは YAML に対応しており、今回は YAML 形式で作成します。
--- - name: <TEST NAME> input: <SAMPLE INPUT> expectations: rules: <RULE NAME>: [PASS|FAIL|SKIP]
input
にはテスト対象とする AWS Config の設定項目を記載します。テストデータとして、デフォルト VPC に Internet Gateway をアタッチしただけの AWS Config の設定項目を利用します。
VPCの設定項目(折りたたんでいます)
version: '1.3' accountId: '111122223333' configurationItemCaptureTime: '2022-10-04T13:59:57.783Z' configurationItemStatus: ResourceDiscovered configurationStateId: '1664891997783' configurationItemMD5Hash: '' arn: 'arn:aws:ec2:ap-northeast-1:111122223333:vpc/vpc-09436d7abf4685717' resourceType: 'AWS::EC2::VPC' resourceId: vpc-09436d7abf4685717 awsRegion: ap-northeast-1 availabilityZone: 'Multiple Availability Zones' tags: Name: test-vpc relatedEvents: { } relationships: - resourceType: 'AWS::EC2::Subnet' resourceId: subnet-0ee9b1ddfe5b4b1a8 relationshipName: 'Contains Subnet' - resourceType: 'AWS::EC2::SecurityGroup' resourceId: sg-019212d5be91ecc89 relationshipName: 'Contains SecurityGroup' - resourceType: 'AWS::EC2::Subnet' resourceId: subnet-051eab0072aa9e630 relationshipName: 'Contains Subnet' - resourceType: 'AWS::EC2::NetworkAcl' resourceId: acl-02cd2667144e8c650 relationshipName: 'Contains NetworkAcl' - resourceType: 'AWS::EC2::RouteTable' resourceId: rtb-0ac24364c616c863a relationshipName: 'Contains RouteTable' - resourceType: 'AWS::EC2::InternetGateway' resourceId: igw-0367be58d3c2fa692 relationshipName: 'Is attached to InternetGateway' - resourceType: 'AWS::EC2::Subnet' resourceId: subnet-0fd4640c7313c55cc relationshipName: 'Contains Subnet' configuration: cidrBlock: 172.31.0.0/16 dhcpOptionsId: dopt-0491e761 state: available vpcId: vpc-09436d7abf4685717 ownerId: '111122223333' instanceTenancy: default ipv6CidrBlockAssociationSet: { } cidrBlockAssociationSet: - associationId: vpc-cidr-assoc-0305a268b56e510bf cidrBlock: 172.31.0.0/16 cidrBlockState: state: associated isDefault: true tags: - key: Name value: test-vpc supplementaryConfiguration: { } resourceTransitionStatus: None
rules
には Guard ルールファイルにおいてテスト対象としたい rule ブロックの名前と期待する評価結果を記載します。評価結果には次の 3 種類があります。
評価結果 | 説明 |
---|---|
PASS | ルールの評価結果が ture (AWS Config ルールにおける準拠) |
FAIL | ルールの評価結果が false (AWS Config ルールにおける非準拠) |
SKIP | ルールがトリガーされていない |
Guard ルールファイルの情報を反映すると単体テストファイルは下記のようになります。期待する評価結果はFAIL
としています。なお、単体テストファイルのサフィックスは_test.yaml
または_tests.yaml
とすることが推奨されています。
--- - name: MyTest input: version: '1.3' accountId: '111122223333' configurationItemCaptureTime: '2022-10-04T13:59:57.783Z' configurationItemStatus: ResourceDiscovered configurationStateId: '1664891997783' configurationItemMD5Hash: '' arn: 'arn:aws:ec2:ap-northeast-1:111122223333:vpc/vpc-09436d7abf4685717' resourceType: 'AWS::EC2::VPC' resourceId: vpc-09436d7abf4685717 awsRegion: ap-northeast-1 availabilityZone: 'Multiple Availability Zones' tags: Name: test-vpc relatedEvents: { } relationships: - resourceType: 'AWS::EC2::Subnet' resourceId: subnet-0ee9b1ddfe5b4b1a8 relationshipName: 'Contains Subnet' - resourceType: 'AWS::EC2::SecurityGroup' resourceId: sg-019212d5be91ecc89 relationshipName: 'Contains SecurityGroup' - resourceType: 'AWS::EC2::Subnet' resourceId: subnet-051eab0072aa9e630 relationshipName: 'Contains Subnet' - resourceType: 'AWS::EC2::NetworkAcl' resourceId: acl-02cd2667144e8c650 relationshipName: 'Contains NetworkAcl' - resourceType: 'AWS::EC2::RouteTable' resourceId: rtb-0ac24364c616c863a relationshipName: 'Contains RouteTable' - resourceType: 'AWS::EC2::InternetGateway' resourceId: igw-0367be58d3c2fa692 relationshipName: 'Is attached to InternetGateway' - resourceType: 'AWS::EC2::Subnet' resourceId: subnet-0fd4640c7313c55cc relationshipName: 'Contains Subnet' configuration: cidrBlock: 172.31.0.0/16 dhcpOptionsId: dopt-0491e761 state: available vpcId: vpc-09436d7abf4685717 ownerId: '111122223333' instanceTenancy: default ipv6CidrBlockAssociationSet: { } cidrBlockAssociationSet: - associationId: vpc-cidr-assoc-0305a268b56e510bf cidrBlock: 172.31.0.0/16 cidrBlockState: state: associated isDefault: true tags: - key: Name value: test-vpc supplementaryConfiguration: { } resourceTransitionStatus: None expectations: rules: no_default_vpc: FAIL
テストを実行する前に環境を整えます。
ローカルで実行するためには、AWS CloudFormation Guard をインストールする必要があります。
https://github.com/aws-cloudformation/cloudformation-guard#installation
Installing AWS CloudFormation Guard - AWS CloudFormation Guard
今回の環境は macOS のため、次のコマンドでインストールしています。
% brew install cloudformation-guard % cfn-guard --version cfn-guard 2.1.0
テストを実行してみます。AWS Config のテストデータはデフォルト VPC のため期待通り非準拠(FAIL
)となっていることを示しています。
% cfn-guard test --rules-file check_default_vpc.guard --test-data check_default_vpc_test.yaml Test Case #1 Name: "MyTest" PASS Rules: no_default_vpc: Expected = FAIL
単体テストファイルを修正して、期待する評価結果をPASS
に変更してみます。
expectations: rules: no_default_vpc: PASS
再度テストを実行したところ、期待した評価結果PASS
に対して、実際の評価結果がFAIL
になっていることが分かります。
% cfn-guard test --rules-file check_default_vpc.guard --test-data check_default_vpc_test.yaml Test Case #1 Name: "MyTest" FAIL Rules: no_default_vpc: Expected = PASS, Evaluated = [FAIL]
オプション-v
, --verbose
で詳細を出力することもできます。
% cfn-guard test --rules-file check_default_vpc.guard --test-data check_default_vpc_test.yaml -v Test Case #1 Name: "MyTest" `- File(, Status=FAIL)[Context=File(rules=1)] `- Rule(no_default_vpc, Status=FAIL)[Context=no_default_vpc] `- GuardClauseBlock(Status = FAIL)[Context=GuardAccessClause#block configuration.isDefault EQUALS false] `- GuardClauseBinaryCheck(Status=FAIL, Comparison= EQUALS, from=(resolved, Path=/configuration/isDefault[L:0,C:0] Value=true), to=(resolved, Path=[L:0,C:0] Value=false))[Context= configuration.isDefault EQUALS false] FAIL Rules: no_default_vpc: Expected = PASS, Evaluated = [FAIL]
以上で、テストの実行は終わりです。
その他 Tips
ここからは Tips の紹介となります。
単体テストファイルは次のように複数のルール評価を記載することができます。
--- - name: <TEST NAME> input: <SAMPLE INPUT> expectations: rules: <RULE NAME>: [PASS|FAIL|SKIP] - name: <TEST NAME> input: <SAMPLE INPUT> expectations: rules: <RULE NAME>: [PASS|FAIL|SKIP]
詳細出力を利用して変数の中身を確認することもできます。
例えば、次の Guard ルールファイルで変数vpc_relationships
の中身を詳細出力から確認できます。評価ルールに意味はありませんのでご注意ください。
let vpc_relationships = relationships[resourceType == "AWS::EC2::InternetGateway"] rule sample_rule { %vpc_relationships empty }
テスト結果に変数の中身が表示されており、意図通り設定項目を絞れているか確認できます。
% cfn-guard test --rules-file sample.guard --test-data sample_test.yaml -v Test Case #1 Name: "MyTest" `- File(, Status=FAIL)[Context=File(rules=1)] `- Rule(sample_rule, Status=FAIL)[Context=sample_rule] `- GuardClauseBlock(Status = FAIL)[Context=GuardAccessClause#block %vpc_relationships EMPTY ] |- Filter/ConjunctionsBlock(Status=FAIL)[Context=Filter/List#1] | `- GuardClauseBlock(Status = FAIL)[Context=GuardAccessClause#block resourceType EQUALS "AWS::EC2::InternetGateway"] | `- GuardClauseBinaryCheck(Status=FAIL, Comparison= EQUALS, from=(resolved, Path=/relationships/0/resourceType[L:0,C:0] Value="AWS::EC2::Subnet"), to=(resolved, Path=[L:0,C:0] Value="AWS::EC2::InternetGateway"))[Context= resourceType EQUALS "AWS::EC2::InternetGateway"] |- Filter/ConjunctionsBlock(Status=FAIL)[Context=Filter/List#1] | `- GuardClauseBlock(Status = FAIL)[Context=GuardAccessClause#block resourceType EQUALS "AWS::EC2::InternetGateway"] | `- GuardClauseBinaryCheck(Status=FAIL, Comparison= EQUALS, from=(resolved, Path=/relationships/1/resourceType[L:0,C:0] Value="AWS::EC2::SecurityGroup"), to=(resolved, Path=[L:0,C:0] Value="AWS::EC2::InternetGateway"))[Context= resourceType EQUALS "AWS::EC2::InternetGateway"] |- Filter/ConjunctionsBlock(Status=FAIL)[Context=Filter/List#1] | `- GuardClauseBlock(Status = FAIL)[Context=GuardAccessClause#block resourceType EQUALS "AWS::EC2::InternetGateway"] | `- GuardClauseBinaryCheck(Status=FAIL, Comparison= EQUALS, from=(resolved, Path=/relationships/2/resourceType[L:0,C:0] Value="AWS::EC2::Subnet"), to=(resolved, Path=[L:0,C:0] Value="AWS::EC2::InternetGateway"))[Context= resourceType EQUALS "AWS::EC2::InternetGateway"] |- Filter/ConjunctionsBlock(Status=FAIL)[Context=Filter/List#1] | `- GuardClauseBlock(Status = FAIL)[Context=GuardAccessClause#block resourceType EQUALS "AWS::EC2::InternetGateway"] | `- GuardClauseBinaryCheck(Status=FAIL, Comparison= EQUALS, from=(resolved, Path=/relationships/3/resourceType[L:0,C:0] Value="AWS::EC2::NetworkAcl"), to=(resolved, Path=[L:0,C:0] Value="AWS::EC2::InternetGateway"))[Context= resourceType EQUALS "AWS::EC2::InternetGateway"] |- Filter/ConjunctionsBlock(Status=FAIL)[Context=Filter/List#1] | `- GuardClauseBlock(Status = FAIL)[Context=GuardAccessClause#block resourceType EQUALS "AWS::EC2::InternetGateway"] | `- GuardClauseBinaryCheck(Status=FAIL, Comparison= EQUALS, from=(resolved, Path=/relationships/4/resourceType[L:0,C:0] Value="AWS::EC2::RouteTable"), to=(resolved, Path=[L:0,C:0] Value="AWS::EC2::InternetGateway"))[Context= resourceType EQUALS "AWS::EC2::InternetGateway"] |- Filter/ConjunctionsBlock(Status=PASS)[Context=Filter/List#1] | `- GuardClauseBlock(Status = PASS)[Context=GuardAccessClause#block resourceType EQUALS "AWS::EC2::InternetGateway"] | `- GuardClauseValueCheck(Status=PASS)[Context= resourceType EQUALS "AWS::EC2::InternetGateway"] |- Filter/ConjunctionsBlock(Status=FAIL)[Context=Filter/List#1] | `- GuardClauseBlock(Status = FAIL)[Context=GuardAccessClause#block resourceType EQUALS "AWS::EC2::InternetGateway"] | `- GuardClauseBinaryCheck(Status=FAIL, Comparison= EQUALS, from=(resolved, Path=/relationships/6/resourceType[L:0,C:0] Value="AWS::EC2::Subnet"), to=(resolved, Path=[L:0,C:0] Value="AWS::EC2::InternetGateway"))[Context= resourceType EQUALS "AWS::EC2::InternetGateway"] `- GuardClauseUnaryCheck(Status=FAIL, Comparison= EMPTY, Value-At=(resolved, Path=/relationships/5[L:0,C:0] Value={"resourceType":"AWS::EC2::InternetGateway","resourceId":"igw-0367be58d3c2fa692","relationshipName":"Is attached to InternetGateway"}))[Context= %vpc_relationships EMPTY ] FAIL Rules: sample_rule: Expected = PASS, Evaluated = [FAIL]
さいごに
AWS CloudFormation Guard の単体テスト(Unit Testing)機能を試してみました。AWS Config カスタムルールを作成する際に毎回 AWS Config の設定をしていては手間がかかるので、ローカル環境でテストできるのはありがたいです。
以上、このブログがどなたかのご参考になれば幸いです。